package mya_dc.compilation_server.compilation;

import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Map.Entry;
import java.io.*;
import mya_dc.shared_classes.*;
import mya_dc.shared_classes.Message.EMessageType;
import mya_dc.shared_classes.externalExecuter.ILogDevice;
import mya_dc.shared_classes.externalExecuter.SysCommandExecutor;
import mya_dc.shared_classes.files_manipulation.FileObject;

/**
 * Class for building a project.
 *
 * <p>
 * <b>Notes:</b>
 * <ul>
 * <li>Class assumes existence of g++ at {root}\MinGW\bin\g++.exe
 * <li>If file doesn't exists <b>FileNotFoundException</b> is thrown
 * <li>Class sends changed files to slaves, gather object files and links objects
 * <li>Class puts result at ResDirectory
 * <li>Compilation errors are kept in m_CompilationErrors
 * <br>To get them use GetBuildErrors()
 * <li>To know if a specific error happened use wasError(ProjectBuilderErrors iError)  
 * <li>After ProjectBuilder dies, binary results, error class and output files will be (created and) kept at ResDirectory
 * </ul>
 * <p>
 * 
 * @author      Yuval Shimron
 * <br>			MYA
 */
public class ProjectBuilder
{
	/**
	 * ProjectBuilderErrors class is used to mark errors in a bit set method
	 * @author Yuval Shimron
	 * <br>	   MYA
	 */
	public class ProjectBuilderErrors {
		// using bit set to know which errors happened.
		final static public int errNoError = 0x0000; 
		final static public int errConnectionToSomeSlavesFailed = 0x0001;
		final static public int errPreprocessingFailed = 0x0002;
		final static public int errCompilationFailed = 0x0004;
		final static public int errSendingFilesToSomeSlaves = 0x008;
		final static public int errGettingFilesFromSomeSlaves = 0x0010;
		final static public int errLinkerFailed = 0x0020;
		final static public int errWarnings = 0x0040;
	}
	
	public final String gcc = System.getProperty("user.dir")+ "\\MinGW\\bin\\g++.exe ";
	private boolean m_bAbortCompilation; // true marks a compilation abortion
	private String strSrc; // Temporary source files directory
	private String strBin; // Binary files (objects) directory
	private String strOut; // Output directory
	private String strErr; // Errors directory
	private String flags;  // compilation flags
	private int m_iNumOfPacks; // inner packages counter
	protected CompilationProject m_ProjectToBuild;
	private Object[] m_listOfSlaves;
	private Transmitter m_clientTransmitter; // client transmitter is used to send notifications
	private int m_Errors; // An accumulating 'bitwise or' of ProjectBuilderErrors errors
	private CompilationErrors m_CompilationErrors;
	private boolean m_threadsDone[]; // keep list of Slave-threads to know when they're done
	private boolean m_someChanged; // true if the are files to compile
	private boolean m_projectChanged;
	LinkedList<Object[]> m_listOfDeletedFiles;
	public CompilationFlags m_compFlags;
	Hashtable<String, String> m_mapFiles; // maps from original path to the binary directory to know
	// which files should be deleted, since all binary files are at the same directory 
	
	/**
	 * Constructor
	 * 
	 * @param ProjectToBuild
	 * @param listOfSlaves
	 * @param ResDirectory - Result directory for output
	 * @param clientTransmitter
	 * @param compFlags
	 * @throws FileNotFoundException - if gcc file doesn't exists
	 */
	public ProjectBuilder (
			CompilationProject ProjectToBuild,
			Object[] listOfSlaves,
			String ResDirectory,
			Transmitter clientTransmitter,
			CompilationFlags compFlags,
			boolean projectChanged,
			LinkedList<Object[]> listOfDeletedFiles
			) throws FileNotFoundException
	{
		if (! new File(gcc).exists())
		{
			throw new FileNotFoundException();
		}
		
		m_compFlags = compFlags;
		m_bAbortCompilation = false;
		m_ProjectToBuild = ProjectToBuild;
		m_projectChanged = projectChanged;
		m_listOfDeletedFiles = listOfDeletedFiles;
		
		m_listOfSlaves = listOfSlaves;
		m_clientTransmitter = clientTransmitter;
		m_Errors = ProjectBuilderErrors.errNoError;
		
		strSrc = ResDirectory + "src\\";
		strBin = ResDirectory + "bin\\";
		strOut = ResDirectory + "output\\";
		strErr = ResDirectory + "errors\\";
		flags  = " " + compFlags.toString() + " -E -o \"" + strSrc;
		
		FileObject.createDirsAccordingToString("", ResDirectory);
		FileObject.createDirsAccordingToString(ResDirectory, "src\\");
		
		m_CompilationErrors = new CompilationErrors();
		m_threadsDone = new boolean[m_listOfSlaves.length];
		loadHashTable();
	}
	
	@SuppressWarnings("unchecked")
	private void loadHashTable()
	{
		String filename = null;
		try 
		{
			filename = strBin + "hashSources.bin";
			if (! (new File(filename)).exists() )
			{
				m_mapFiles = new Hashtable<String, String>();
				return;
			}
			ObjectInputStream inStream = new ObjectInputStream(new FileInputStream(filename));
			m_mapFiles = (Hashtable<String, String>)inStream.readObject();
			inStream.close();
			clearDeletedObjectFiles();
		}
		catch (Exception e)
		{
			FileObject.deleteDirectory(new File(filename));
			m_mapFiles = new Hashtable<String, String>();
		}		
	}
	
	private void clearDeletedObjectFiles()
	{
		for (Object[] filename : m_listOfDeletedFiles)
		{
			try {
				String filepath = (String)filename[0];
				if ((Boolean)filename[1] == true) // was file
				{
					FileObject.deleteDirectory(new File(strBin + m_mapFiles.get(filepath)));
					m_mapFiles.remove((String)filename[0]);
				}
				else // was directory
				{
					Set<Entry<String, String>> set = m_mapFiles.entrySet();
					LinkedList<String> binFilesToDelete = new LinkedList<String>();
					LinkedList<String> keysToRemove = new LinkedList<String>();
					for (Entry<String, String> entry : set)
					{
						if (entry.getKey().indexOf(filepath) == 0)
						{
							// directory is substring of the file
							// deleting file...
							binFilesToDelete.add(entry.getKey());
							keysToRemove.add(entry.getKey());
						}
					}
					for (String srcFile : binFilesToDelete)
					{
						String binFile = strBin + m_mapFiles.get(srcFile);
						FileObject.deleteDirectory(new File(binFile));
					}
					for (String key : keysToRemove)
					{
						m_mapFiles.remove(key);
					}
				}
			}
			catch (Exception e) { }
		}	
	}

	private void saveHashTable()
	{
		try 
		{
			new File(strBin).mkdirs();
			String filename = strBin + "hashSources.bin";
			ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream(filename));
			outStream.writeObject(m_mapFiles);		
			outStream.flush();
			outStream.close();
		}
		catch (Exception e)
		{
			
		}		
	}

	private void loadErrorClass()
	{
		try 
		{
			String filename = strErr + "errorClass.bin";
			if (! (new File(filename)).exists() )
			{
				m_CompilationErrors = new CompilationErrors();
				return;
			}
			ObjectInputStream inStream = new ObjectInputStream(new FileInputStream(filename));
			m_CompilationErrors = (CompilationErrors)inStream.readObject();
			m_Errors = (Integer)inStream.readObject();
			inStream.close();
		}
		catch (Exception e)
		{
			FileObject.deleteDirectory(new File(strErr));
			m_CompilationErrors = new CompilationErrors();
			m_Errors = 0;
		}		
	}
	
	private void saveErrorClass()
	{
		try 
		{
			new File(strErr).mkdirs();
			String filename = strErr + "errorClass.bin";
			ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream(filename));
			outStream.writeObject(m_CompilationErrors);	
			outStream.writeObject(m_Errors);
			outStream.flush();
			outStream.close();
		}
		catch (Exception e)
		{
			FileObject.deleteDirectory(new File(strErr));
		}		
	}
	
	/**
	 * Marks abortion of compilation process
	 */
	public synchronized void abortCompilation()
	{
		m_bAbortCompilation = true;
	}
	
	/**
	 * Returns abortion state of compilation process
	 */
	private synchronized boolean compilationAborted()
	{
		return m_bAbortCompilation;
	}
	
	/**
	 * Main build function
	 * 
	 * @return true if "errNoError" received at end of process
	 */
	public boolean doBuild()
	{
		if (!m_projectChanged)
		{
			return noCompile();
		}
		
		makeSources();
		if (compilationAborted())
		{
			doCleanUp();
			return false;
		}
		if (m_someChanged == false && m_listOfDeletedFiles.isEmpty())
		{
			return noCompile(); // Build is up to date
		}
		if (m_someChanged)
		{
			buildPackages();
		}
		if (compilationAborted())
		{
			doCleanUp();
			return false;
		}

		if (m_someChanged)
		{
			compilePackages(); 
		}
		if (compilationAborted())
		{
			doCleanUp();
			return false;
		}
		linkPackages();
		doCleanUp();
		try {
				m_clientTransmitter.send(new Message(
					EMessageType.COMPILATION_DONE, 
					null
				)
			);
		}
		catch (Exception e) {}
		return (wasError(ProjectBuilderErrors.errNoError));
	}
	
	private boolean noCompile() {
		try {
			m_clientTransmitter.send(new Message(
					EMessageType.COMPILATION_NUM_OF_SLAVES, 
					1
				)
			);
			m_clientTransmitter.send(new Message(
					EMessageType.SLAVEISDONE,
					null
				)
			);
			m_clientTransmitter.send(new Message(
					EMessageType.COMPILATION_DONE, 
					null
				)
			);
		}
		catch (Exception e) {}	
		loadErrorClass();
		return true;
	}

	/**
	 * Makes directory of pre-processed files in StrSrc 
	 * 
	 * @return true if and only if all went well.
	 */
	boolean makeSources()
	{
		LinkedList<MYA_File> listOfFilesToBuild = m_ProjectToBuild.getFiles(MYA_FileTreeNode.FilesToGet.OUT_DATED_FILES);
		// (re)Building only out of dated objects...
		
		if (listOfFilesToBuild.isEmpty())
		{
			m_someChanged = false;
			return true; // no out dated files
		}
		
		FileObject.deleteDirectory((new File(strOut)));
		m_someChanged = true;
		// if a header file is changed rebuilding the whole project.
		boolean bHeaderChanged = false;
		for(MYA_File currFile : listOfFilesToBuild)
		{
			String filename = currFile.getName();
			String ext = (filename.lastIndexOf(".") == -1 )? "": filename.substring(filename.lastIndexOf(".")+1,filename.length());
			if (ext.equals("h") || ext.equals("hpp"))
				bHeaderChanged = true;
		}
		if (bHeaderChanged)
		{
			listOfFilesToBuild = m_ProjectToBuild.getFiles(MYA_FileTreeNode.FilesToGet.ALL_FILES);
		}
		
		SysCommandExecutor cmdExecutor = new SysCommandExecutor();
		ILogDevice ErrorLog = new ILogDevice();
		cmdExecutor.setOutputLogDevice(null);
		cmdExecutor.setErrorLogDevice(ErrorLog);
		int exitStatus;
		boolean result = true; // true means no errors in pre-processing found
		
		for(MYA_File currFile : listOfFilesToBuild)
		{
			if (compilationAborted())
				return result;
			
			String filename = currFile.getName();
			String ext = (filename.lastIndexOf(".") == -1 )? "": filename.substring(filename.lastIndexOf(".")+1,filename.length());
			if (!ext.equals("c") && !ext.equals("cpp") && !ext.equals("cc"))
			{
				String relative = currFile.getPath().substring(m_ProjectToBuild.getProjectWD().length());
				String[] path = relative.split("\\\\");
				MYA_File file = m_ProjectToBuild.getChild(path);
				if (file != null)
				{
					// compiling only *.c,*.cpp,*.cc files
					file.setLastTransmitted();
				}
				continue;
			}
			
			exitStatus = 0;
			
			try {
				String FileName = currFile.getName().substring(0,filename.lastIndexOf("."));
				int i = 1;
				while (new File(strSrc + FileName + ".c").isFile() ||
					   new File(strSrc + FileName + ".cc").isFile() ||
					   new File(strSrc + FileName + ".cpp").isFile())
				{
					// if filename exists creates duplicates: fileName(1).cpp, fileName(2).cc, ...  
					FileName += "(" + Integer.toString(i++) + ")";
				}
				FileName += "." + ext;
				FileObject.deleteDirectory(new File(strSrc + m_mapFiles.get(currFile.getPath()))); // delete previous object result (if exist by mistake)
				m_mapFiles.put(currFile.getPath(), currFile.getName().substring(0,filename.lastIndexOf("."))+ ".o");
				ErrorLog.m_Errors.clear();
				exitStatus = cmdExecutor.runCommand(gcc + "\"" + currFile.toString() + "\"" + flags +  FileName + "\"");
			}
			
			catch(Exception e) {
			}
			
			if (exitStatus == 1)
			{
				m_Errors |= ProjectBuilderErrors.errPreprocessingFailed;
				addErrors(ErrorLog.m_Errors);
			}
			else if (!ErrorLog.m_Errors.isEmpty())
			{
				addErrors(ErrorLog.m_Errors);
				m_Errors |= ProjectBuilderErrors.errWarnings;
			}
		}

		saveHashTable();
		return result;
	}
	
	/**
	 * Creates a directory for each compilation package that will be send to a Salve.
	 * <br>Number of packages will be approx. total size of files divided by number of Slaves.
	 * 
	 * @return true if and only if all went well.
	 */
	boolean buildPackages()
	{
		MYA_File srcDir = new MYA_File(strSrc);
		MYA_File[] files = srcDir.listFiles();
		int iTotSize = 0;
		for (MYA_File currFile : files)
		{
			iTotSize += currFile.length();
		}
		
		if (iTotSize == 0)
		{
			try {
				// send client number of packages for the progress bar
				m_clientTransmitter.send(new Message(
						EMessageType.COMPILATION_NUM_OF_SLAVES, 
						1
					)
				);
				m_clientTransmitter.send(new Message(
						EMessageType.SLAVEISDONE,
						null
					)
				);
			}
			catch (Exception e) {}	
			return true;
		}
		
		int iPackSize = iTotSize/m_listOfSlaves.length;
		int iCounter = 0, iCurrSize = 0;
		for (MYA_File currFile : files)
		{
			if (compilationAborted())
				return false;
			if (currFile.isDirectory() == true)
				continue;
			iCurrSize += currFile.length();
			FileObject.createDirsAccordingToString(strSrc, "pack" + Integer.toString(iCounter) + "\\");
			currFile.renameTo(new File(strSrc + "pack" + Integer.toString(iCounter) + "\\", currFile.getName())); 
			if (iCurrSize >= iPackSize)
			{
				iCurrSize = 0;
				if (files[files.length-1] != currFile)
					iCounter++; // don't increase if last file.
			}
		}
		
		m_iNumOfPacks = iCounter+1;
		if (compilationAborted())
			return false;
		try {
			// send client number of packages for the progress bar
			m_clientTransmitter.send(new Message(
					EMessageType.COMPILATION_NUM_OF_SLAVES, 
					m_iNumOfPacks
				)
			);
		}
		catch (Exception e) {}	
		return true;
	}
	
	/**
	 * Activates Slave-threads to compile packages
	 * 
	 * @return true if and only if all went well
	 */
	boolean compilePackages()
	{
		MYA_File srcDir = new MYA_File(strSrc);
		MYA_File[] files = srcDir.listFiles();
		if (files.length == 0)
			return true;
		// nothing to compile
		
		String packDir = strSrc + "pack";
		CompilationThread threads[] = new CompilationThread[m_iNumOfPacks];
		int tryOut[] = new int[m_listOfSlaves.length];
		// when a Slave fails we try other slaves until no more available.
		// This array help monitoring that.
		
		for(int i = 0 ; i < m_iNumOfPacks ; i++)
		{
			threads[i] = new CompilationThread((ConnectInfo)m_listOfSlaves[i], packDir, strBin, i, this);
			m_threadsDone[i] = false;
			tryOut[i] = -1;
			threads[i].start();	
		}
		
		// wait for threads to finish...
		boolean done = false;
		while (!done)
		{
			done = true;
			if (compilationAborted())
			{
				for (int i=0 ; i < m_iNumOfPacks ; i++ )
				{
					try {
						if (threads[i].isAlive())
							threads[i].abortCompilation();
					} catch (Exception e) {}
				}
				return false;
			}
			try {
				Thread.sleep(1000);
				for (int i=0 ; i < m_iNumOfPacks ; i++ )
				{	
					// each thread marks itself done in m_threadsDone array using synchronized markDone() function
					if (!isDone(i))
					{
						done = false;
						if (!threads[i].isAlive())
						{
							// try another slave (first try same slave)
							tryOut[i]++;
							if (tryOut[i] >= m_listOfSlaves.length)
							{
								// no more slaves available
								setError(ProjectBuilderErrors.errSendingFilesToSomeSlaves);
								return false;
							}
							threads[i] = 
								new CompilationThread(
									(ConnectInfo)m_listOfSlaves[(i + tryOut[i]) % m_listOfSlaves.length],
									packDir, 
									strBin, 
									i, 
									this
								);
							threads[i].start();
						}
					}
				}
			} 
			catch (InterruptedException ie)
			{
				/* ignored */
			}
			catch (Exception e)
			{ 
				setError(ProjectBuilderErrors.errSendingFilesToSomeSlaves);
				return false;
			}
		}
		
		if (((m_Errors & ProjectBuilderErrors.errCompilationFailed)         > 0) ||
			((m_Errors & ProjectBuilderErrors.errSendingFilesToSomeSlaves)  > 0) ||
			((m_Errors & ProjectBuilderErrors.errGettingFilesFromSomeSlaves)> 0) )
			return false;

		return true;
	}
	
	/**
	 * Each thread that successfully received result from a Slave marks itself done
	 * 
	 * @param pos
	 */
	public synchronized void markDone(int pos)
	{
		m_threadsDone[pos] = true;
		// send progress message to client:
		if (!m_bAbortCompilation) try {
			Message notification = new Message(EMessageType.COMPILATION_SLAVE_DONE, pos);
			m_clientTransmitter.send(notification);
		}
		catch (Exception e) {}
	}
	
	public synchronized boolean isDone(int pos)
	{
		return m_threadsDone[pos];
	}
	
	/**
	 * Links all object results to an output file or adds linkage errors
	 * 
	 * @return
	 */
	boolean linkPackages()
	{
		SysCommandExecutor cmdExecutor = new SysCommandExecutor();
		ILogDevice ErrorLog = new ILogDevice();
		cmdExecutor.setOutputLogDevice(null);
		cmdExecutor.setErrorLogDevice(ErrorLog);
		int exitStatus = 0;
		boolean result = true;
		cmdExecutor.setWorkingDirectory(strBin);
		
		MYA_File binDir = new MYA_File(strBin);
		try {
			if (!binDir.exists())
			{
				return true;
				// nothing to link
			}
			MYA_File[] files = binDir.listFiles();
			if (files.length == 0)
			{
				return true;
				// nothing to link
			}
			if (compilationAborted())
				return false;
			
			new File(strOut).mkdirs();
			
			// linking  objects that are stored in {root}\bin\ directory
			ErrorLog.m_Errors.clear();
			exitStatus = cmdExecutor.runCommand(gcc + "*.o " +  m_compFlags.toString() + " -o \"" + strOut + m_compFlags.outputFileName + "\"");
		}
		
		catch(Exception e) {
			return false;
		}
		
		if (exitStatus == 1)
		{
			m_Errors |= ProjectBuilderErrors.errLinkerFailed;
			addErrors(ErrorLog.m_Errors);
			result = false;
		}
		else if (!ErrorLog.m_Errors.isEmpty())
		{
			addErrors(ErrorLog.m_Errors);
			m_Errors |= ProjectBuilderErrors.errWarnings;
		}
		return result;
	}
	
	/**
	 * Mark source as compiled if (and only if) object exists
	 */
	public void markCompiled()
	{
		Set<Entry<String, String>> set = m_mapFiles.entrySet();
		for (Entry<String, String> entry : set)
		{
			if (new File(strBin + entry.getValue()).isFile())
			{
				String relative = entry.getKey().substring(m_ProjectToBuild.getProjectWD().length());
				String[] path = relative.split("\\\\");
				MYA_File file = m_ProjectToBuild.getChild(path);
				if (file == null)
				{
					continue;
				}
				file.setLastTransmitted();
			}
		}
	}

	/**
	 * Deletes temporary pre-processed files. Keep Output file (if exist)
	 * 
	 */
	void doCleanUp()
	{
		if ((new File(strSrc)).isDirectory())
		{
			FileObject.deleteDirectory(new File(strSrc));
		}
		saveErrorClass();
	}
		
	/**
	 * @return true if error "iError" (one of ProjectBuilderErrors) happened
	 * 
	 */
	public synchronized boolean wasError(int iError)
	{
		if (iError == ProjectBuilderErrors.errNoError && m_Errors == 0)
			return true;
		return (m_Errors & iError) > 0;
	}
	
	/**
	 * Sets error
	 */
	public synchronized void setError(int iError)
	{
		m_Errors |= iError;
	}
	
	/**
	 * Adds errors to the CompilationErrors class that were received from complier
	 * 
	 * @param errClass
	 */
	public synchronized void addErrors(CompilationErrors errClass)
	{
		for (MyaCompilationError error : errClass)
			m_CompilationErrors.addLast(error);
	}
	
	/**
	 * @return CompilationErrors class in a thread safe way
	 */
	private synchronized CompilationErrors getErrorClass()
	{
		return m_CompilationErrors;
	}
	
	/**
	 * @return Compilation errors class. Tries to eliminate irrelevant errors and local directories from errors
	 * 
	 */
	public CompilationErrors GetBuildErrors()
	{
		if (getErrorClass() == null)
			return null;
		if (getErrorClass().isEmpty())
			return null;
		for (MyaCompilationError error : getErrorClass())
		{
			int pos;
			// Eliminate project path
			while ((pos = error.m_strError.indexOf(m_ProjectToBuild.getProjectWD())) >= 0) {
				error.m_strError = 
					error.m_strError.substring(0, pos) + 
					error.m_strError.substring(pos + m_ProjectToBuild.getProjectWD().length());
			}
			// Eliminate System.getProperty("user.dir") path
			while ((pos = error.m_strError.indexOf(System.getProperty("user.dir"))) >= 0) {
				error.m_strError = 
					error.m_strError.substring(0, pos) + 
					error.m_strError.substring(pos + System.getProperty("user.dir").length());
			}		
			if (!(new File(error.m_strFileName).isFile())) {
				if ((pos = error.m_strError.indexOf("In file included from ")) >=0 )
				{
					String tmp = error.m_strError.substring(pos+"In file included from ".length());
					StringTokenizer Tok = new StringTokenizer(tmp,":");
			        
			        if (Tok.hasMoreElements())
			        	error.m_strFileName = Tok.nextElement().toString();
				
				 	if (Tok.hasMoreElements())
				 		error.m_strLineNum = Tok.nextElement().toString(); 
				 		String tmp2 = error.m_strLineNum;
				 		StringTokenizer Tok2 = new StringTokenizer(tmp2,",");
				 		error.m_strLineNum = Tok2.nextElement().toString(); 
				 		try {
				 			Integer.parseInt(error.m_strLineNum);
				 			// might not contain a number since sometimes errors don't give a line #.
				 		} catch (Exception e) {
				 			error.m_strLineNum = "";
				 		}
				}
				else
				{
					error.m_strFileName = "";
					error.m_strLineNum = "";
				}
			} else {
				// Eliminate project path
				while ((pos = error.m_strFileName.indexOf(m_ProjectToBuild.getProjectWD())) >= 0) {
					error.m_strFileName = 
						error.m_strFileName.substring(0, pos) + 
						error.m_strFileName.substring(pos + m_ProjectToBuild.getProjectWD().length());
				}
				// Eliminate System.getProperty("user.dir") path
				while ((pos = error.m_strFileName.indexOf(System.getProperty("user.dir"))) >= 0) {
					error.m_strFileName = 
						error.m_strFileName.substring(0, pos) + 
						error.m_strFileName.substring(pos + System.getProperty("user.dir").length());
				}
			}
		}
		saveErrorClass();
		return getErrorClass();
	}
}
